#!/usr/bin/env python
"""
Copyright (C) 2009 pk

This work is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; version 2.

This work is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
version 2 of the GNU General Public License for more details.
"""

import os, sys
import re
from subprocess import Popen,PIPE
from optparse import OptionParser
import platform
import xmlrpclib
import socket
import threading
import time
import kthread

from SimpleXMLRPCServer import SimpleXMLRPCServer
from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler


webdir = '.'   # where your HTML files and cgi-bin script directory live
_master_port = 1590
_server_port = 1591
_ip = socket.gethostbyname(socket.gethostname())


parser = OptionParser()


def add_options():
    parser.add_option("-i", "--ip", dest="ip", default = _ip,
                      help="Ip to bind to")
    parser.add_option("-p", "--port",
                      dest="port", default= _server_port,
                      help="Port to bind to")
    parser.add_option("-t", "--type",
                      dest="type",
                      help = "Server type")
    parser.add_option("-n", "--name",
                      dest="name",
                      help = "Server name. Defaults to short hostname")
    parser.add_option("-m", "--master",
                      dest = "master",
                      help = "Portage-master hostname/ip to connect to")
    parser.add_option("-s", "--standalone",
                      dest = "alone",
                      action = "store_true",
                      default = False,
                      help = "Run as standalone server. Does not connect to portage-master")


class PortageService(object):
    """
    Class to create an XMLRPCServer on given ip and port
    and export functions in given object
    """

    def __init__(self, ip, port, export_functions):
        self.running = True
        self.ip = ip
        self.port = port
        self.export_functions = export_functions
        class RequestHandler(SimpleXMLRPCRequestHandler):
            rpc_paths = ('/RPC2',)

        self.server = SimpleXMLRPCServer((self.ip, self.port),
                                         requestHandler=RequestHandler, allow_none = True)
        self.server.register_multicall_functions()
        self.server.register_introspection_functions()
        self.server.register_instance(self.export_functions)
        threading.Thread(target = self.serve, args = []).start()

        
    def serve(self):
        try:
            self.server.serve_forever()
        except KeyboardInterrupt:
            logger.info("Exiting..")
            sys.exit(0)


class BasePortage(SimpleXMLRPCServer):
    """
    BasePortage sublclass of SimpleXMLRPCServer.
    Creates an rpc server on specified port and ip and registers
    functions defined in class ExportFunctions via xml-rpc
    """
    # Default portage-master hostname
    _portage_master = "portage-master"

    # Server name defaults to node name(short hostname)
    name = platform.node()


    class ExportFunctions(object):
        """
        Define functions to be exported via xml-rpc
        """
        
        # Package status
        _status = {
            'R' : 'Installed',
            'N' : 'Not installed',
            'U' : 'Update available'
            }

        def __init__(self, server):
            self.server = server


    def connect_master(self, ip, port):
        self.master = xmlrpclib.ServerProxy('http://' + str(ip) + ':' + str(port), verbose=False)
        self.master.add_server(self.name, self.ip, self.type)
        

    def __init__(self, ip, port, type, name = None, alone = False, master = None):
        self.ip = ip
        self.port = port
        if name:
            self.name = name
        self.type = type
        if master:
            self.portage_master = master
        else:
            self.portage_master = self._portage_master
        
        class RequestHandler(SimpleXMLRPCRequestHandler):
            rpc_paths = ('/RPC2',)
        SimpleXMLRPCServer.__init__(self, (self.ip, self.port),
                                    requestHandler=RequestHandler, allow_none = True)
        self.register_multicall_functions()
        self.register_introspection_functions()
        self.register_instance(self.ExportFunctions(self))
        if not alone:
            self.connect_master(self.portage_master, _master_port)
        self.service = kthread.KThread(target = self.serve, args = [])
        self.service.daemon = False
        self.service.start()

        
    def serve(self):
        try:
            self.serve_forever()
        except KeyboardInterrupt:
            logger.info("Exiting..")
            sys.exit(0)



# Register a function under a different name
class HelperDefs(object):
    lines_want = re.compile('(>>>.+\n)+')

    @staticmethod
    def split_build_output(_output):
        return HelperDefs.lines_want.match(_output).group()

    @staticmethod
    def make_package_dir(_category_package_ver):
        # category/package_name-ver-some-more-ver
        seperator = '/'
        package_sep = '-'
        temp = _category_package_ver.split(seperator)
        category = temp[0]
        package_ver = temp[1]
        pkg = package_ver.split(package_sep)
        package_name = pkg[0]
        ver = pkg[1:]
        del temp

        package_dir = PortageServer._eprefix + PortageServer._usr_portage + seperator
        pkg_items = [category, package_name, package_ver + '.ebuild']
        pkg_line = '/'.join(pkg_items)

        return package_dir + pkg_line

    @staticmethod
    def make_category(_category_package_ver):
        seperator = '/'
        package_sep = '-'
        temp = _category_package_ver.split(seperator)
        category = temp[0]
        package_ver = temp[1]
        pkg = package_ver.split(package_sep)
        package_name = pkg[0]
        del temp, package_ver

        return category + '/' + package_name
    
    @staticmethod
    def add_line_to_file(file, line):
        fileHandle = open(file, 'a')
        fileHandle.write(line)
        fileHandle.close()
        return True

    @staticmethod        
    def run_command(_args):
        p = Popen(_args, stdout=PIPE, stderr=PIPE)
        stdout, stderr = p.communicate()
        if bool(stdout) and bool(stderr):
            return stdout + stderr
        elif bool(stdout):
            return stdout
        elif bool(stderr):
            return stderr


    @staticmethod
    def write_file(filepath, mode, data):
        try:
            with open(filepath, mode) as file:
                file.write(data)
            return True
        except IOError, e:
            print e
        return False


# Register an instance; all the methods of the instance are
# published as XML-RPC methods
class PortageServer(BasePortage):
    _portage_options = ['--color=n', '-q', '--nospinner', '--getbinpkgonly']
    _portage_cmd = 'emerge'
    _FEATURES = "nocolor notitles"
    _eprefix = os.getenv('EPREFIX')
    if not _eprefix:
        _eprefix = '/'
    _usr_portage = '/usr/portage'
    _package_keywords_filepath = _eprefix + '/etc/portage/package.keywords'
    _package_mask_filepath = _eprefix + '/etc/portage/package.mask'
    _package_unmask_filepath = _eprefix + '/etc/portage/package.unmask'
    _package_use_filepath = _eprefix + '/etc/portage/package.use'
    _make_conf_filepath = _eprefix + '/etc/make.conf'
    _etc_profile_filepath = _eprefix + '/etc/profile'


    def __init__(self, ip, port, type, name = None, alone = False, master = None):
        # super(PortageServer, self).__init__(ip, port)
        BasePortage.__init__(self, ip, port, type, name, alone = alone, master = master)
        # self.server.register_instance(self)
        # self.server.serve_forever()

    def _exit(self):
        time.sleep(5)
        self.service.kill()
        sys.exit(0)

    def service_stop(self):
        threading.Thread(target = self._exit, args = []).start()
        return True

    def portage_pretend(self, package):
        command = ['--pretend']
        command.extend(self._portage_options)
        command.append(package)
        command.insert(0,self._portage_cmd)
        print command
        return HelperDefs.run_command(command)

    def get_package_status(self, package):
        status = self.portage_pretend(package)
        items = status.split(' ')
        status = items[3]
        return self._status[status]


    def ebuild_package(self, package):
        command = []
        full_name = HelperDefs.make_package_dir(self.get_latest_version(package))
            
        command.append(full_name)
        command.append('package')
        command.insert(0,'ebuild')
        return self._uni.sub('', HelperDefs.run_command(command))


    def get_latest_version(self, package):
        command = ['-up']
        command.extend(self._portage_options)
        command.append(package)
        command.insert(0,self._portage_cmd)
        
        output = self.portage_pretend(package)
        output = output.strip()
        
        words = output.split(' ')
        
        return words[len(words)-1]


    def portage_update_pretend(self, package):
        command = ['-up']
        command.extend(self._portage_options)
        command.append(package)
        command.insert(0,self._portage_cmd)
        return HelperDefs.run_command(command)


    def portage_update(self, package):
        command = ['-u']
        command.extend(self._portage_options)
        command.append(package)
        command.insert(0,self._portage_cmd)
        return HelperDefs.run_command(command)


    def portage_install(self, package):
        command = []
        command.extend(self._portage_options)
        command.append(package)
        command.insert(0,self._portage_cmd)
        return HelperDefs.split_build_output(HelperDefs.run_command(command))


    def portage_sync(self):
        command = ['--sync', '--quiet']
        command.extend(self._portage_options)
        command.insert(0,self._portage_cmd)
        return HelperDefs.run_command(command)


    def portage_update_world(self):
        command = ['-u']
        command.extend(self._portage_options)
        command.append('world')
        command.insert(0,self._portage_cmd)
        return HelperDefs.run_command(command)


    def portage_update_system(self):
        command = ['-u']
        command.extend(self._portage_options)
        command.append('system')
        command.insert(0,self._portage_cmd)

        output = HelperDefs.run_command(command)
        if output.strip() == '':
            return "No outdated packages were found on your system."
        return output


    def portage_verbose(self, package, _args = None):
        command = ['-v']
        if _args:
            command.extend(_args)
        command.extend(self._portage_options)
        command.append(package)
        command.insert(0,self._portage_cmd)
        return HelperDefs.run_command(command)


    def portage_remove(self, package):
        command = ['-C']
        command.extend(self._portage_options)
        command.append(package)
        command.insert(0,self._portage_cmd)
        return HelperDefs.run_command(command)


    def get_uname(self):
        command = ['uname', '-a']
        return HelperDefs.run_command(command)


    def get_df(self):
        command = ['df', '-kh']
        return HelperDefs.run_command(command)
        

    class ExportFunctions(object):
        _portage_options = ['--color=n', '-q', '--nospinner', '--getbinpkgonly']
        _portage_cmd = 'emerge'
        _FEATURES = "nocolor notitles"
        
        # Some unicode values that we need to get rid of because they can not be
        # parsed by xml and break the rpc call
        _uni = re.compile('\x08')
        # \x1b[32;01mok\x1b[34;01m ]\x1b[0m\n \x1b[32;01m*\x1b[0m
        _ebuild_uni = re.compile('\x1b')

        def __init__(self, server):
            self.server = server


        def get_df(self):
            return self.server.get_df(self)


        def get_uname(self, args):
            return self.server.get_uname(args)
        

        def get_package_status(self, package):
            return self.server.get_package_status(package)


        def ebuild_pkg(self, package):
            return self.server.ebuild_pkg(package)


        def get_latest_version(self, package):
            return self.server.get_latest_version(package)

        # ebuild /usr/portage/net-dns/dnsmasq/dnsmasq-2.34.ebuild package

        def portage_verbose(self, package, _args = None):
            return self.server.portage_verbose(package, _args = None)

            
        def portage_get_use_flags(self, package):
            verbose = self.portage_verbose(package, ['-p'])
            return verbose.split('"')[1].split(' ') 


        def portage_update_pretend(self, package):
            return self.server.portage_update_pretend(package)


        def portage_update(self, package):
            return self.server.portage_update(package)

            
        def portage_update_pretend_world(self):
            return self.portage_update_pretend('world')


        def portage_install(self, package):
            return self.server.portage_install(package)


        def portage_remove(self, package):
            return self.server.portage_remove(package)


        def portage_sync(self):
            return self.server.portage_sync()

                    
        def portage_pretend(self, package):
            return self.server.portage_pretend(package)
            

        def portage_update_world(self):
            return self.server.portage_update_world()
            
            
        def portage_update_system(self):
            return self.server.portage_update_system()


        def push_make_conf_file(self, data):
            return HelperDefs.write_file(PortageServer._make_conf_filepath, 'w', data)

        
        def push_package_keywords_file(self, data):
            return HelperDefs.write_file(PortageServer._package_keywords_filepath, 'w', data)


        def push_package_use_file(self, data):
            return HelperDefs.write_file(PortageServer._package_use_filepath, 'w', data)


        def push_package_mask_file(self, data):
            return HelperDefs.write_file(PortageServer._package_mask_filepath, 'w', data)


        def push_package_unmask_file(self, data):
            return HelperDefs.write_file(PortageServer._package_unmask_filepath, 'w', data)


        def push_etc_profile(self, data):
            return HelperDefs.write_file(PortageServer._etc_profile_filepath, 'w', data)


        def service_stop(self):
            return self.server.service_stop()
            

        def portage_push_files(self, data):
            """
            data is hash with filename as key that points to data
            corresponding to that file
            """
            success = True
            file_to_def = { 'make.conf' : self.push_make_conf_file,
                            'package.keywords' : self.push_package_keywords_file,
                            'package.use' : self.push_package_use_file,
                            'package.mask' : self.push_package_mask_file,
                            'package.unmask': self.push_package_unmask_file,
                            'profile' : self.push_etc_profile }
            for file in data:
                print "File: %s" %(file)
                if file in file_to_def:
                    # file_to_def[file](data[file])
                    # print data[file]
                    if not file_to_def[file](data[file]):
                        print "%s not found in file_to_def" %(file)
                        # if not HelperDefs.write_file(file_to_def[file](data[file]), 'w', data[file]):
                        success = False
                else:
                    print("'%s' is not a valid file." %(file))
                    success = False
            # print "Exiting.."
            return success
                
#         def portage_add_keyword_package(self, package):
#             full_name = self.make_package_fullname(package)
#             line = '\n' + full_name + ' **'
#             if HelperDefs.add_line_to_file(self._package_keywords_filepath, line):
#                 return True
#             return False

        def make_package_fullname(self, package):
            return HelperDefs.make_category(self.get_latest_version(package))


add_options()


def main():
    (options, args) = parser.parse_args()
    if not options.type:
        print "You must provide a server type"
        sys.exit(0)
    else:
        if options.master and options.alone:
            print "Can't both connect to a master and run as stand alone server. Pick one."
            sys.exit(1)
        elif options.master:
            PortageServer(options.ip, options.port, options.type, master = options.master)
        elif options.alone:
            PortageServer(options.ip, options.port, options.type, alone = True)
        else:
            PortageServer(options.ip, options.port, options.type, BasePortage._portage_master,)

def test():
    server = PortageServer('localhost', _server_port, 'prod', alone = True)
    time.sleep(5)
    print server.service_stop()
    # print server.portage_pretend('portage')
    # print server.portage_sync()

main()
# test()
